package pprg;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;

public class DiningPhilosophers extends ReentrantLock {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private int n;
	private long eatTime;
	private long thinkTime;

	private Philosopher[] philosophers;
	private Fork[] forks;
	ExecutorService executorService;

	private static long startTime;
	private static long lastTime;
	private static boolean terminated = false;

	private boolean preventDeadlock;
	private static boolean testEatingTimes = false;
	private static boolean testTimesTillDeadlock = false;
	private static boolean createSingleDiningPhilosopersProblem = true;

	private static final int RUNTIME_NO_DEADLOCKS = 10000;

	public static void main(String[] args) {

		DiningPhilosophers d;

		if (testEatingTimes) {
			testEatingTimeDistribution(20, 4, 4, 10, false, "eatingTimes_WithDeadLock_20_4_4_10.csv");
		}

		if (testTimesTillDeadlock) {
			testTimesTillDeadlock(2, 10, 4, 4, 10, "timesTillDeadlock_2_10_4_4_10.csv");
		}

		if (createSingleDiningPhilosopersProblem) {
			int p = Util.readInput("Enter number of philosophs: ",
					"Please enter a number!");
			int e = Util.readInput("Enter maximum eating time (ms): ",
					"Please enter a number!");
			int t = Util.readInput("Enter maximum thinking time (ms): ",
					"Please enter a number!");

			d = new DiningPhilosophers(p, e, t, false);
			d.start();
		}
	}

	public DiningPhilosophers(int n, long e, long t, boolean preventDeadlock) {
		this.n = n;
		this.eatTime = e;
		this.thinkTime = t;
		terminated = false;
		this.preventDeadlock = preventDeadlock;
		init();
	}

	private void init() {
		philosophers = new Philosopher[n];
		forks = new Fork[n];
		for (int i = 0; i < n; i++) {
			forks[i] = new Fork(i);
		}
		for (int i = 0; i < n; i++) {
			ForkOrder forkOrder;
			if (preventDeadlock) {
				if (i == 0) {
					forkOrder = new ForkOrder(forks[(i + 1) % n], forks[i]);
				} else {
					forkOrder = new ForkOrder(forks[i], forks[(i + 1) % n]);
				}
			} else {
				forkOrder = new ForkOrder(forks[i], forks[(i + 1) % n]);
			}
			philosophers[i] = new Philosopher(i, forkOrder, thinkTime, eatTime);
		}
	}

	public synchronized long start() {
		startTime = System.currentTimeMillis();
		lastTime = startTime;
		executorService = Executors.newFixedThreadPool(n);
		for (Philosopher p : philosophers) {
			executorService.execute(p);
		}

		if (preventDeadlock) {
			while ((System.currentTimeMillis() - startTime) < RUNTIME_NO_DEADLOCKS) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		} else {
			while ((System.currentTimeMillis() - startTime) - lastTime < 100) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}

		executorService.shutdown();
		terminated = true;

		for (Philosopher p : philosophers) {
			p.setTerminated(true);
			System.out.println(String.format(
					"Eating time of %s = %s. He ate %s times", p.toString(),
					p.getTotalEatingTime(), p.getTimesEaten()));
		}

		System.out.println("TIME until deadlock was reached: " + lastTime
				+ "ms");

		return lastTime;
	}

	public static long getRuntime() {
		if (!terminated) {
			lastTime = System.currentTimeMillis() - startTime;
		}
		return lastTime;
	}

	private static void testTimesTillDeadlock(int minNumberPhilosophers,
			int maxNumberPhilopsphers, int eatingTime, int thinkingTime,
			int numberCalculations, String fileName) {

		Map<Integer, Long> times = new HashMap<Integer, Long>();
		DiningPhilosophers d;
		for (int i = minNumberPhilosophers; i <= maxNumberPhilopsphers; i++) {
			long sumTime = 0;
			for (int j = 0; j < numberCalculations; j++) {
				d = new DiningPhilosophers(i, eatingTime, thinkingTime, false);
				synchronized (d) {
					sumTime += d.start();
				}
			}
			times.put(i, sumTime / numberCalculations);
		}

		for (Integer i : times.keySet()) {
			System.out.println(String.format(
					"%s philospohers : Mean time to deadlock = %s", i,
					times.get(i)));
		}

		Util.writeDeadlockTimesToCsvFile(fileName, times, 10);
	}

	private static void testEatingTimeDistribution(int numberPhilopsphers,
			long eatingTime, long thinkingTime, int numberCalculations,
			boolean preventDeadlock, String fileName) {

		Map<Integer, Long> actualEatingTimesPerPhilopspher = new HashMap<Integer, Long>();
		Map<Integer, Integer> actualEatingAmountsPerPhilopsopher = new HashMap<Integer, Integer>();

		for (int i = 0; i < numberCalculations; i++) {
			DiningPhilosophers d = new DiningPhilosophers(numberPhilopsphers,
					eatingTime, thinkingTime, preventDeadlock);

			synchronized (d) {
				d.start();
			}

			for (Philosopher p : d.philosophers) {
				if (actualEatingAmountsPerPhilopsopher
						.containsKey(p.getIndex())) {
					actualEatingAmountsPerPhilopsopher
							.put(p.getIndex(),
									actualEatingAmountsPerPhilopsopher.get(p
											.getIndex()) + p.getTimesEaten());
				} else {
					actualEatingAmountsPerPhilopsopher.put(p.getIndex(),
							p.getTimesEaten());
				}
				if (actualEatingTimesPerPhilopspher.containsKey(p.getIndex())) {
					actualEatingTimesPerPhilopspher.put(p.getIndex(),
							actualEatingTimesPerPhilopspher.get(p.getIndex())
									+ p.getTotalEatingTime());
				} else {
					actualEatingTimesPerPhilopspher.put(p.getIndex(),
							p.getTotalEatingTime());
				}
			}

		}

		Map<Integer, Long> averageEatingTimesPerPhilosopher = new HashMap<Integer, Long>();
		Map<Integer, Integer> averageEatingAmountsPerPhilosopher = new HashMap<Integer, Integer>();

		for (Integer index : actualEatingAmountsPerPhilopsopher.keySet()) {
			averageEatingTimesPerPhilosopher.put(index,
					actualEatingTimesPerPhilopspher.get(index)
							/ numberCalculations);
			averageEatingAmountsPerPhilosopher.put(index,
					actualEatingAmountsPerPhilopsopher.get(index)
							/ numberCalculations);
		}

		Util.writeEatingTimesEvaluationToCsvFile(
				fileName,
				averageEatingTimesPerPhilosopher,
				averageEatingAmountsPerPhilosopher, numberCalculations);

	}
}
